Coverage Report

Created: 2026-06-19 16:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\utils\config.rs
Line
Count
Source
1
//! Client and Daemon configuration structs.
2
3
use serde_derive::{Deserialize, Serialize};
4
use std::env;
5
use windows::Win32::System::Console::{
6
    BACKGROUND_BLUE, BACKGROUND_INTENSITY, BACKGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN,
7
    FOREGROUND_INTENSITY, FOREGROUND_RED,
8
};
9
10
/// Behavior when an arrow / `hjkl` keystroke would move the
11
/// enable/disable submenu's selection past the edge of the client grid.
12
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
13
#[serde(rename_all = "snake_case")]
14
pub enum EdgeBehavior {
15
    /// Keep the current selection on edge keystrokes (default).
16
    Clamp,
17
    /// Wrap to the opposite edge of the same row (Left/Right) or column
18
    /// (Up/Down).
19
    Wrap,
20
}
21
22
impl Default for EdgeBehavior {
23
77
    fn default() -> Self {
24
77
        return EdgeBehavior::Clamp;
25
77
    }
26
}
27
28
/// Default console color applied when a client is in the
29
/// `Disabled` state.
30
///
31
/// Default-grey foreground (red+green+blue, no intensity) on a
32
/// `BACKGROUND_INTENSITY`-only background paints the window as light text on
33
/// a muted dark-grey background - a clear "this client is greyed out" cue
34
/// that stays visually distinct from the daemon's bright-red palette.
35
const DEFAULT_DISABLED_CONSOLE_COLOR: u16 =
36
    FOREGROUND_RED.0 | FOREGROUND_GREEN.0 | FOREGROUND_BLUE.0 | BACKGROUND_INTENSITY.0;
37
38
/// Default console color for the daemon's currently selected submenu
39
/// client: bright-white on blue, distinct from the daemon's bright-red
40
/// and the muted disabled palettes so it stands out at a glance.
41
const DEFAULT_HIGHLIGHTED_CONSOLE_COLOR: u16 = FOREGROUND_RED.0
42
    | FOREGROUND_GREEN.0
43
    | FOREGROUND_BLUE.0
44
    | FOREGROUND_INTENSITY.0
45
    | BACKGROUND_BLUE.0;
46
47
/// Placeholder for the `<username>@<host>` argument to the chosen SSH program.
48
const DEFAULT_USERNAME_HOST_PLACEHOLDER: &str = "{{USERNAME_AT_HOST}}";
49
50
/// Representation of the project configuration.
51
///
52
/// Includes subcommand specific configurations for `client` and `daemon` subcommands
53
/// as well es the cluster tags.
54
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
55
pub struct Config {
56
    /// List of cluster tags.
57
    ///
58
    /// Includes the name of the cluster tag and a list of hostnames.
59
    pub clusters: Vec<Cluster>,
60
    /// Configuration relevant for the `client` subcommand.
61
    pub client: ClientConfig,
62
    /// Configuration relevant for the `daemon` subcommand.
63
    pub daemon: DaemonConfig,
64
}
65
66
/// Representation of the project configuration
67
/// where everything is optional.
68
///
69
/// Used to handle cases where only some or none of the configurations are present.
70
/// Enables backwards compatiblity with configuration files written by older versions.
71
#[derive(Serialize, Deserialize, Default)]
72
pub struct ConfigOpt {
73
    #[allow(missing_docs)]
74
    pub clusters: Option<Vec<Cluster>>,
75
    #[allow(missing_docs)]
76
    pub client: Option<ClientConfigOpt>,
77
    #[allow(missing_docs)]
78
    pub daemon: Option<DaemonConfigOpt>,
79
}
80
81
impl From<ConfigOpt> for Config {
82
    /// Unwraps the existing configuration values or applies the default.
83
15
    fn from(val: ConfigOpt) -> Self {
84
15
        return Config {
85
15
            clusters: val.clusters.unwrap_or_default(),
86
15
            client: val.client.unwrap_or_default().into(),
87
15
            daemon: val.daemon.unwrap_or_default().into(),
88
15
        };
89
15
    }
90
}
91
92
impl From<Config> for ConfigOpt {
93
    /// Wraps all configuration values as options.
94
1
    fn from(val: Config) -> Self {
95
1
        return ConfigOpt {
96
1
            clusters: Some(val.clusters),
97
1
            client: Some(val.client.into()),
98
1
            daemon: Some(val.daemon.into()),
99
1
        };
100
1
    }
101
}
102
103
/// Representation of a cluster tag.
104
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
105
pub struct Cluster {
106
    /// Name of the cluster tag, used to identify it.
107
    pub name: String,
108
    /// List of hostnames the cluster tag is an alias for.
109
    pub hosts: Vec<String>,
110
}
111
112
/// Representation of the `client` subcommand configurations.
113
#[derive(Serialize, Deserialize, PartialEq, Debug)]
114
pub struct ClientConfig {
115
    /// Full path to the SSH config.
116
    ///
117
    /// # Example
118
    ///
119
    /// `'C:\Users\<username>\.ssh\config'`
120
    pub ssh_config_path: String,
121
    /// Name of the program used to establish the SSH connection.
122
    /// # Example
123
    ///
124
    /// `'ssh'`
125
    pub program: String,
126
    /// List of arguments provided to the program.
127
    ///
128
    /// Must include the `username_host_placeholder`.
129
    ///
130
    /// # Example
131
    ///
132
    /// `['-XY', '{{USERNAME_AT_HOST}}']`
133
    pub arguments: Vec<String>,
134
    /// Placeholder string used to inject `<user>@<host>` into the list of arguments.
135
    ///
136
    /// # Example
137
    ///
138
    /// `'{{USERNAME_AT_HOST}}'`
139
    pub username_host_placeholder: String,
140
    /// Controls back- and foreground colors of the client console window
141
    /// when the client is in the `Disabled` state.
142
    ///
143
    /// Uses the same encoding as [`DaemonConfig::console_color`].
144
    /// All [standard Windows color combinations][1] are available:
145
    ///
146
    /// FOREGROUND_BLUE:        1   \
147
    /// FOREGROUND_GREEN:       2   \
148
    /// FOREGROUND_RED:         4   \
149
    /// FOREGROUND_INTENSITY:   8   \
150
    /// BACKGROUND_BLUE:        16  \
151
    /// BACKGROUND_GREEN:       32  \
152
    /// BACKGROUND_RED:         64  \
153
    /// BACKGROUND_INTENSITY:   128 \
154
    ///
155
    /// # Example
156
    ///
157
    /// Default-grey font on muted dark-grey background:
158
    /// 4 + 2 + 1 + 128 = `135`
159
    ///
160
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
161
    pub disabled_console_color: u16,
162
    /// Controls back- and foreground colors of the client console window
163
    /// while it is the currently selected window in the daemon's
164
    /// enable/disable submenu.
165
    ///
166
    /// Uses the same encoding as [`DaemonConfig::console_color`].
167
    /// All [standard Windows color combinations][1] are available:
168
    ///
169
    /// FOREGROUND_BLUE:        1   \
170
    /// FOREGROUND_GREEN:       2   \
171
    /// FOREGROUND_RED:         4   \
172
    /// FOREGROUND_INTENSITY:   8   \
173
    /// BACKGROUND_BLUE:        16  \
174
    /// BACKGROUND_GREEN:       32  \
175
    /// BACKGROUND_RED:         64  \
176
    /// BACKGROUND_INTENSITY:   128 \
177
    ///
178
    /// # Example
179
    ///
180
    /// Bright-white font on blue background:
181
    /// 4 + 2 + 1 + 8 + 16 = `31`
182
    ///
183
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
184
    pub highlighted_console_color: u16,
185
}
186
187
impl Default for ClientConfig {
188
    /// Returns a sensible default `ClientConfig`.
189
    ///
190
    /// # Returns
191
    ///
192
    /// `ClientConfig` with the following values:
193
    /// * `ssh_config_path`             - `%USERPROFILE%\.ssh\config`
194
    /// * `program`                     - `ssh`
195
    /// * `arguments`                   - `-XY {{USERNAME_AT_HOST}}`
196
    /// * `username_host_placeholder`   - `{{USERNAME_AT_HOST}}`
197
    /// * `disabled_console_color`      - `135`
198
    /// * `highlighted_console_color`   - `31`
199
    ///
200
    /// Note: %USERPROFILE% actually is resolved by us, so the actual value
201
    ///       is whatever the environment variable at runtime points to.
202
78
    fn default() -> Self {
203
78
        return ClientConfig {
204
78
            ssh_config_path: format!("{}\\.ssh\\config", env::var("USERPROFILE").unwrap()),
205
78
            program: "ssh".to_string(),
206
78
            arguments: vec![
207
78
                "-XY".to_string(),
208
78
                DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
209
78
            ],
210
78
            username_host_placeholder: DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
211
78
            disabled_console_color: DEFAULT_DISABLED_CONSOLE_COLOR,
212
78
            highlighted_console_color: DEFAULT_HIGHLIGHTED_CONSOLE_COLOR,
213
78
        };
214
78
    }
215
}
216
217
/// Representation of the `client` subcommand configurations
218
/// where everything is optional.
219
#[derive(Serialize, Deserialize)]
220
pub struct ClientConfigOpt {
221
    #[allow(missing_docs)]
222
    pub ssh_config_path: Option<String>,
223
    #[allow(missing_docs)]
224
    pub program: Option<String>,
225
    #[allow(missing_docs)]
226
    pub arguments: Option<Vec<String>>,
227
    #[allow(missing_docs)]
228
    pub username_host_placeholder: Option<String>,
229
    #[allow(missing_docs)]
230
    pub disabled_console_color: Option<u16>,
231
    #[allow(missing_docs)]
232
    pub highlighted_console_color: Option<u16>,
233
}
234
235
impl Default for ClientConfigOpt {
236
13
    fn default() -> Self {
237
13
        return ClientConfig::default().into();
238
13
    }
239
}
240
241
impl From<ClientConfigOpt> for ClientConfig {
242
    /// Unwraps the existing configuration values or applies the default.
243
19
    fn from(val: ClientConfigOpt) -> Self {
244
19
        let default = ClientConfig::default();
245
19
        return ClientConfig {
246
19
            ssh_config_path: val.ssh_config_path.unwrap_or(default.ssh_config_path),
247
19
            program: val.program.unwrap_or(default.program),
248
19
            arguments: val.arguments.unwrap_or(default.arguments),
249
19
            username_host_placeholder: val
250
19
                .username_host_placeholder
251
19
                .unwrap_or(default.username_host_placeholder),
252
19
            disabled_console_color: val
253
19
                .disabled_console_color
254
19
                .unwrap_or(default.disabled_console_color),
255
19
            highlighted_console_color: val
256
19
                .highlighted_console_color
257
19
                .unwrap_or(default.highlighted_console_color),
258
19
        };
259
19
    }
260
}
261
262
impl From<ClientConfig> for ClientConfigOpt {
263
    /// Wraps all configuration values as options.
264
14
    fn from(val: ClientConfig) -> Self {
265
14
        return ClientConfigOpt {
266
14
            ssh_config_path: Some(val.ssh_config_path),
267
14
            program: Some(val.program),
268
14
            arguments: Some(val.arguments),
269
14
            username_host_placeholder: Some(val.username_host_placeholder),
270
14
            disabled_console_color: Some(val.disabled_console_color),
271
14
            highlighted_console_color: Some(val.highlighted_console_color),
272
14
        };
273
14
    }
274
}
275
276
/// Representation of the `daemon` subcommand configurations.
277
#[derive(Serialize, Deserialize, PartialEq, Debug)]
278
pub struct DaemonConfig {
279
    /// Height in pixel of the daemon console window.
280
    ///
281
    /// Note: we are [DPI Unaware][1] which means the number of pixels
282
    ///       represents the `logical` scale, not the physical.
283
    ///
284
    /// [1]: https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#dpi-unaware
285
    pub height: i32,
286
    /// Controls how the client console windows make use of the available screen space.
287
    ///
288
    /// * `> 0.0` - Aims for vertical rectangle shape.
289
    ///             The larger the value, the more exaggerated the "verticality".
290
    ///             Eventually the windows will all be columns.
291
    /// * `= 0.0` - Aims for square shape.
292
    /// * `< 0.0` - Aims for horizontal rectangle shape.
293
    ///             The smaller the value, the more exaggerated the "horizontality".
294
    ///             Eventually the windows will all be rows.
295
    ///             `-1.0` is the sweetspot for mostly preserving a 16:9 ratio.
296
    #[serde(alias = "aspect_ratio_adjustement")]
297
    pub aspect_ratio_adjustment: f64,
298
    /// Controls back- and foreground colors of the daemon console window.
299
    ///
300
    /// All [standard Windows color combinations][1] are available:
301
    ///
302
    /// FOREGROUND_BLUE:        1   \
303
    /// FOREGROUND_GREEN:       2   \
304
    /// FOREGROUND_RED:         4   \
305
    /// FOREGROUND_INTENSITY:   8   \
306
    /// BACKGROUND_BLUE:        16  \
307
    /// BACKGROUND_GREEN:       32  \
308
    /// BACKGROUND_RED:         64  \
309
    /// BACKGROUND_INTENSITY:   128 \
310
    ///
311
    /// # Example
312
    ///
313
    /// White font on red background: 8 + 4 + 2 + 1 + 128 + 64 = `207`
314
    ///
315
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
316
    pub console_color: u16,
317
    /// Behavior when an arrow / `hjkl` keystroke would move the
318
    /// enable/disable submenu's selection past the edge of the client grid.
319
    ///
320
    /// * `clamp` (default) - keep the current selection.
321
    /// * `wrap` - wrap to the opposite edge of the same row (Left/Right)
322
    ///   or column (Up/Down).
323
    pub submenu_edge_behavior: EdgeBehavior,
324
}
325
326
impl Default for DaemonConfig {
327
    /// Returns a sensible default `DaemonConfig`.
328
    ///
329
    /// # Returns
330
    ///
331
    /// `DaemonConfig` with the following values:
332
    /// * `height`                      - `200`
333
    /// * `aspect_ratio_adjustment`    - `-1.0`
334
    /// * `console_color`               - `207`
335
    /// * `submenu_edge_behavior`       - `clamp`
336
77
    fn default() -> Self {
337
77
        return DaemonConfig {
338
77
            height: 200,
339
77
            aspect_ratio_adjustment: -1f64,
340
77
            console_color: (FOREGROUND_INTENSITY
341
77
                | FOREGROUND_RED
342
77
                | FOREGROUND_GREEN
343
77
                | FOREGROUND_BLUE
344
77
                | BACKGROUND_INTENSITY
345
77
                | BACKGROUND_RED)
346
77
                .0,
347
77
            submenu_edge_behavior: EdgeBehavior::default(),
348
77
        };
349
77
    }
350
}
351
352
/// Representation of the `daemon` subcommand configurations
353
/// where everything is optional.
354
#[derive(Serialize, Deserialize)]
355
pub struct DaemonConfigOpt {
356
    #[allow(missing_docs)]
357
    pub height: Option<i32>,
358
    #[allow(missing_docs)]
359
    #[serde(alias = "aspect_ratio_adjustement")]
360
    pub aspect_ratio_adjustment: Option<f64>,
361
    #[allow(missing_docs)]
362
    pub console_color: Option<u16>,
363
    #[allow(missing_docs)]
364
    pub submenu_edge_behavior: Option<EdgeBehavior>,
365
}
366
367
impl Default for DaemonConfigOpt {
368
12
    fn default() -> Self {
369
12
        return DaemonConfig::default().into();
370
12
    }
371
}
372
373
impl From<DaemonConfigOpt> for DaemonConfig {
374
    /// Unwraps the existing configuration values or applies the default.
375
19
    fn from(val: DaemonConfigOpt) -> Self {
376
19
        let default = DaemonConfig::default();
377
19
        return DaemonConfig {
378
19
            height: val.height.unwrap_or(default.height),
379
19
            aspect_ratio_adjustment: val
380
19
                .aspect_ratio_adjustment
381
19
                .unwrap_or(default.aspect_ratio_adjustment),
382
19
            console_color: val.console_color.unwrap_or(default.console_color),
383
19
            submenu_edge_behavior: val
384
19
                .submenu_edge_behavior
385
19
                .unwrap_or(default.submenu_edge_behavior),
386
19
        };
387
19
    }
388
}
389
390
impl From<DaemonConfig> for DaemonConfigOpt {
391
    /// Wraps all configuration values as options.
392
13
    fn from(val: DaemonConfig) -> Self {
393
13
        return DaemonConfigOpt {
394
13
            height: Some(val.height),
395
13
            aspect_ratio_adjustment: Some(val.aspect_ratio_adjustment),
396
13
            console_color: Some(val.console_color),
397
13
            submenu_edge_behavior: Some(val.submenu_edge_behavior),
398
13
        };
399
13
    }
400
}
401
402
#[cfg(test)]
403
#[path = "../tests/utils/test_config.rs"]
404
mod test_config;